Skip to content

feat(mcp): an agent can finally read back what it recorded to the SD card - #511

Merged
tylerkron merged 4 commits into
mainfrom
feat/mcp-sd-data-tools-500
Aug 13, 2026
Merged

feat(mcp): an agent can finally read back what it recorded to the SD card#511
tylerkron merged 4 commits into
mainfrom
feat/mcp-sd-data-tools-500

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

What was wrong

An agent driving a DAQiFi through the MCP server could start an SD recording it could never read. The server had start_sd_logging and stop_sd_logging and nothing else: no way to see what was on the card, no way to check whether there was room to record, no way to fetch a log back, and no way to turn one into something it could actually analyse. The card was a write-only device. Ask "log for 10 seconds and tell me the average on AI0" and the agent could do the first half and then had nothing.

Core has shipped the whole retrieval surface for a long time — listing, storage, download, delete, and format-detecting parsers that feed the CSV exporter. None of it was reachable from a tool call.

How it was fixed

Four tools over the Core APIs that were already there: list_sd_files, get_sd_storage, download_sd_file and delete_sd_file. download_sd_file fetches the raw file and, by default, parses it and writes a CSV beside it, returning both paths — so one call closes the loop from "there is a file on the card" to "here is a table I can read".

What a reviewer may want to push back on:

  • download_sd_file is not blocked by --read-only; only delete_sd_file is. Reading data off the card changes nothing on the device, and a read-only server that cannot read the data would be a strange thing. It does write two files into this machine's temp directory, which is the only way the bytes can reach the agent at all. Stated in the tool description and the README rather than left implicit.
  • A CSV that fails to parse does not fail the tool call. The download can take minutes; throwing away a successful transfer because the parse step tripped would just make the agent download it again to learn the same thing. The raw path comes back either way and csvError says what happened. The one case that does fail is a log that parses to zero samples, because a header-only CSV is indistinguishable from a successful export of an empty file — that one deletes the stub and says so.
  • A file name that is not on the card is now refused up front. Asking the firmware for a file that does not exist produces no answer at all, and Core gives up 20 seconds later with "the device stopped feeding the transfer; retry the download" — advice for the one thing that cannot work. The tool now checks the listing first (free when the caller listed already, one re-listing on a miss so a just-recorded file is never wrongly rejected) and answers immediately with the names that are there. Matching ignores case and the card's own spelling is what goes to the firmware.
  • A one-line fix in Core came with it. SdCardDeviceConfiguration.FromDevice folded the live Channels view, which DaqifiDevice documents as unsafe off the consumer thread — a status message repopulating the collection mid-enumeration throws "Collection was modified", and it read the collection twice so the analog and digital counts could disagree. It now takes one lock-protected snapshot. This is on the download path (the live device is what supplies the timestamp clock that firmware ≤3.7.2 omits from SD logs), so the MCP server would hit it; the example app calls it in the same place.
  • The CSV is written as <download>.csv, not by swapping the extension. The firmware logs in CSV as well as protobuf, and Core's temp file keeps the device-side extension — so ChangeExtension(".csv") names the very file being read and truncates the download on the way to parsing it. A test pins it.

Verification

Tests — 40 new (33 in Daqifi.Mcp.Tests, 7 in Daqifi.Core.Tests). Two are proven regression catchers: reverting the Core snapshot fix fails FromDevice_WhileStatusMessagesRepopulateChannels_DoesNotThrow 2/2 runs, and restoring ChangeExtension fails CsvSourceFile_IsNotOverwrittenByItsOwnExport. The parse-and-export chain is covered end to end against a synthetic on-disk log, and the reported CSV row count is checked against the lines Core's real exporter writes rather than against the rule the counter was written from. Full suite green on net9.0 (2964 Core + 88 Mcp) and net10.0 (2964), 0 failures, 0 warnings.

Bench (non-destructive), Nq1 fw 3.7.2 on /dev/cu.usbmodem1101 — driven as a real MCP server over stdio JSON-RPC, not through the agent class:

  • All 19 tools register; the complete loop ran through tool calls: configure_analog_channels [0]set_sample_rate 50start_sd_logging → 1.2 s → stop_sd_logginglist_sd_files (46→47 files, the new log_20260812_165615.bin listed at 430 B) → download_sd_file430 B fetched in 0.68 s, 43 samples, 43 CSV rows, reported count equal to the CSV's actual data lines, timestamps 20 ms apart as commanded (which is also the check that the live device's 42 MHz clock reached the parser — the 50 MHz fallback would have stretched them ~19%).
  • get_sd_storage 7.80 GB total / 100% free; list_sd_files 45–47 entries with sizes and dates; the same file downloaded again under a fully upper-cased name and resolved; does_not_exist.bin refused in 2.1 s naming what is on the card, where before the fix it stalled for 20 s.
  • Two firmware conditions were hit and are reported as such rather than as stack traces: files ≥3.7 KB come back as marker-only empty transfers (the known SD-buffer collapse, #703 — 186 B and 274 B files served fine in 0.68 s), and a missing file stalls the transfer. Both surface as Core's typed exceptions with the MCP-specific next step appended.
  • The --read-only refusals are unit-tested only. The bench run of that server did not complete: after the stalled transfers the device stopped answering SCPI (discovery intermittent, then silent; connect times out at the channel-configuration wait). Confirmed device-side, not a regression — the example CLI fails identically on the same port, and the same MCP binary had connected six times earlier in the session. It is the documented transient state whose recovery is a replug. Nothing here writes to the device on that path anyway: the read-only refusal throws before any I/O.
  • No reboot, no format, no delete, no firmware, no LAN writes. One 1.2 s log file was written to the card, which is what the issue's first success criterion asks for.

closes #500

Not merging — this is for your review.

An agent could start an SD recording through MCP but had no way to see
what was on the card or get any of it back. Adds list_sd_files,
get_sd_storage, download_sd_file (raw file + parsed CSV) and
delete_sd_file over Core's existing SD surface, closing the loop
start -> stop -> list -> download -> CSV.

Also fixes SdCardDeviceConfiguration.FromDevice, which folded the live
Channels view and could throw "Collection was modified" when the
consumer thread repopulated channels underneath it.

closes #500

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron
tylerkron requested a review from a team as a code owner August 12, 2026 23:05
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat(mcp): add SD-card retrieval tools with optional CSV export

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Expose SD-card retrieval over MCP: list files, check storage, download logs, and delete files.
• Optionally parse downloaded logs into CSV and return row/sample counts plus parse warnings.
• Fix Core SD config snapshotting to avoid concurrent channel enumeration failures.
Diagram

graph TD
  A["MCP Client"] --> B["MCP Tools"] --> C["DaqifiAgent"] --> D["Core SD APIs"] --> E{{"DAQiFi + SD"}}
  C --> F[("Temp files")]
  C --> G["Parse + CSV export"] --> F
  subgraph Legend
    direction LR
    _ext{{"Hardware"}} ~~~ _svc["Code module"] ~~~ _fs[("Local FS")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split download vs export into separate MCP tools
  • ➕ Clearer semantics: download is always pure transfer; export is explicit post-processing
  • ➕ Allows retries of CSV parsing without re-downloading large files
  • ➖ More tool calls and more state to pass around (raw path + device-side filename + config hints)
  • ➖ Agents may forget the second step; current default closes the loop in one call
2. Return file bytes via MCP resources/attachments instead of temp paths
  • ➕ Avoids relying on local temp paths and file accessibility assumptions
  • ➕ Potentially more portable across deployments/containers
  • ➖ May be impractical for large logs (memory/transport overhead)
  • ➖ Requires additional MCP protocol/resource implementation complexity; temp-path handoff is simplest today
3. Gate download under --read-only like other SD actions
  • ➕ Strict interpretation of read-only mode
  • ➕ Avoids local disk writes in read-only deployments
  • ➖ Conflicts with the intent of read-only as “safe observation”; prevents retrieving data needed for analysis
  • ➖ Still requires some persistence mechanism to get bytes to the agent; current behavior documents the temp write clearly

Recommendation: Keep the PR’s approach: a single download tool that can optionally export CSV is the right UX for agents and minimizes repeated long transfers. The temp-file side effect is acceptable given it’s documented and required for handoff, and delete remains correctly gated by --read-only. The only follow-up worth considering is whether a future MCP “resource download” path is needed for deployments where local temp paths aren’t accessible to the client.

Files changed (9) +1137 / -7

Enhancement (4) +526 / -0
DaqifiAgent.csImplement SD retrieval operations and CSV export pipeline +326/-0

Implement SD retrieval operations and CSV export pipeline

• Adds ListSdFilesAsync, GetSdStorageAsync, DownloadSdFileAsync, and DeleteSdFileAsync, including MCP-friendly exception rewriting and read-only enforcement only for destructive delete. Implements filename preflight resolution, snapshots live device config for timestamp parsing, and adds an ExportCsvAsync helper that detects format from device-side names and writes CSV next to the raw download while reporting counts/warnings.

src/Daqifi.Mcp/DaqifiAgent.cs

Dtos.csAdd DTOs for SD listing, storage, download, and delete results +59/-0

Add DTOs for SD listing, storage, download, and delete results

• Introduces SdFileEntry, SdFileListing, SdStorageReport, SdDownloadReport, and SdDeleteResult to structure tool outputs and encode important semantics like unknown size vs empty file and CSV error reporting.

src/Daqifi.Mcp/Dtos.cs

SdCardSampleSource.csAdd ISampleSource adapter to export parsed SD logs as CSV +106/-0

Add ISampleSource adapter to export parsed SD logs as CSV

• Implements an ISampleSource over parsed SdCardLogEntry streams so Core’s CsvExporter can write CSVs. Tracks sample count vs row count (timestamp-based), always exports a digital column, and reports dropped analog columns when log width exceeds known channel count.

src/Daqifi.Mcp/SdCardSampleSource.cs

DaqifiTools.csExpose SD retrieval tools via MCP tool surface +35/-0

Expose SD retrieval tools via MCP tool surface

• Adds MCP tool definitions for list_sd_files, get_sd_storage, download_sd_file (with exportCsv default true), and delete_sd_file with clear descriptions and GuardAsync wrapping.

src/Daqifi.Mcp/Tools/DaqifiTools.cs

Bug fix (1) +11 / -2
SdCardDeviceConfiguration.csFix FromDevice to snapshot channels to avoid concurrent modification +11/-2

Fix FromDevice to snapshot channels to avoid concurrent modification

• Adds null guarding and switches enumeration from the live Channels view to GetChannelsSnapshot(). Ensures analog and digital counts come from the same snapshot and avoids "Collection was modified" failures.

src/Daqifi.Core/Device/SdCard/SdCardDeviceConfiguration.cs

Tests (2) +579 / -0
SdCardDeviceConfigurationTests.csAdd regression tests for SdCardDeviceConfiguration.FromDevice snapshotting +84/-0

Add regression tests for SdCardDeviceConfiguration.FromDevice snapshotting

• Introduces unit and concurrency/regression tests ensuring FromDevice validates null input, reports analog/digital counts, and does not throw when channels are repopulated concurrently.

src/Daqifi.Core.Tests/Device/SdCard/SdCardDeviceConfigurationTests.cs

SdCardToolsTests.csAdd MCP contract + regression tests for SD retrieval + CSV export +495/-0

Add MCP contract + regression tests for SD retrieval + CSV export

• Adds extensive tests covering read-only gating, unknown device messaging, filename validation and resolution, DTO behavior, SdCardSampleSource semantics, and end-to-end CSV export against synthetic on-disk logs.

src/Daqifi.Mcp.Tests/SdCardToolsTests.cs

Documentation (2) +21 / -5
README.mdMention MCP SD data retrieval in top-level README +2/-2

Mention MCP SD data retrieval in top-level README

• Updates the project overview text to clarify that the MCP server can retrieve SD logs back (including CSV conversion), not only start/stop SD logging.

README.md

README.mdDocument new SD tools and read-only behavior +19/-3

Document new SD tools and read-only behavior

• Adds tool documentation for SD listing/storage/download/delete, clarifies that downloads write to local temp, and documents operational guidance (retrieve before streaming) plus which operations are blocked by --read-only.

src/Daqifi.Mcp/README.md

@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Sync CSV output stream ✓ Resolved 🐞 Bug ➹ Performance
Description
ExportCsvAsync opens the CSV output FileStream with default (non-async) options while
CsvExporter.ExportAsync performs many WriteAsync calls; this can fall back to synchronous file
I/O / thread-pool-assisted writes and reduce scalability and cancellation responsiveness during
large exports.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R865-868]

+                var fileStream = new FileStream(csvPath, FileMode.Create, FileAccess.Write, FileShare.Read);
+                await using (fileStream.ConfigureAwait(false))
+                {
+                    var writer = new StreamWriter(fileStream);
Relevance

●●● Strong

They favor true-async patterns; output stream should match async exporter writes for
scalability/cancellation.

PR-#94
PR-#400

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new code creates the CSV output stream using the non-async FileStream constructor, but the
exporter uses async write APIs throughout, so the export path is not truly async on the output side.

src/Daqifi.Mcp/DaqifiAgent.cs[865-873]
src/Daqifi.Core/Logging/Export/CsvExporter.cs[28-33]
src/Daqifi.Core/Logging/Export/CsvExporter.cs[90-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ExportCsvAsync` writes CSV using `CsvExporter.ExportAsync(...)` which uses async `TextWriter.WriteAsync` heavily, but the CSV output `FileStream` is created without `useAsync: true` (and without an explicit buffer size). For large SD logs, this can degrade throughput and responsiveness because the async export can end up performing synchronous file writes (often via thread-pool assistance), and cancellation may be less responsive.

### Issue Context
Input reading already uses `useAsync: true`; output should be consistent.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[865-874]

### Proposed fix
Create the CSV `FileStream` with `useAsync: true` and an explicit buffer size, e.g.:
```csharp
var fileStream = new FileStream(
   csvPath,
   FileMode.Create,
   FileAccess.Write,
   FileShare.Read,
   bufferSize: 64 * 1024, // or a shared constant
   useAsync: true);
```
Optionally also construct `StreamWriter` with an explicit encoding/buffer if desired.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Orphaned CSV on export error ✓ Resolved 🐞 Bug ☼ Reliability
Description
ExportCsvAsync creates localPath + ".csv" before exporting; if CSV export/writes throw, the
partially-written CSV is left on disk. DownloadSdFileAsync then swallows the exception and returns
CsvPath=null, so callers can’t discover or clean up the orphaned temp file.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R857-860]

+            var csvPath = localPath + ".csv";
+            var fileStream = new FileStream(csvPath, FileMode.Create, FileAccess.Write, FileShare.Read);
+            await using (fileStream.ConfigureAwait(false))
+            {
Relevance

●●● Strong

Temp-file cleanup on export failure is a straightforward reliability fix; consistent with existing
CSV deletion on zero-samples.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CSV is created with FileMode.Create and exported into, but only the zero-sample case deletes
it; other exceptions propagate. The caller-level tool catches export exceptions and returns without
a CSV path, which makes any partially-created CSV file impossible for the caller to clean up.

src/Daqifi.Mcp/DaqifiAgent.cs[857-878]
src/Daqifi.Mcp/DaqifiAgent.cs[695-713]
src/Daqifi.Mcp/Dtos.cs[214-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ExportCsvAsync` can leave a partially written `*.csv` file behind when `CsvExporter.ExportAsync(...)` (or stream writing) throws. Since `DownloadSdFileAsync` catches and converts those failures into a successful tool response with `CsvPath=null`, the orphaned file becomes undiscoverable and accumulates in temp storage.

### Issue Context
- The CSV file is created eagerly (`FileMode.Create`) and only deleted in the special `SampleCount == 0` case.
- Any other exception during the export path leaves the file on disk.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[857-889]
- src/Daqifi.Mcp/DaqifiAgent.cs[695-713]

### Implementation notes
- Wrap the CSV creation/export block in a `try/catch` that calls `TryDelete(csvPath)` on *any* exception (including IO exceptions).
- Prefer writing to a temp name (e.g., `localPath + ".csv.tmp"`) and `File.Move(..., overwrite:true)` only after a successful export; on failure, delete the temp.
- Keep `OperationCanceledException` behavior consistent (either delete the temp file before rethrow or ensure the temp-name approach prevents litter).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Duplicated buffer size constant ✓ Resolved 🐞 Bug ⚙ Maintainability ⭐ New
Description
CsvWriteBufferBytes duplicates the existing default parser buffer size
(SdCardParseOptions.BufferSize = 64 KB), creating a drift risk where future tuning changes one side
but not the other and leaves the "matching" comment inaccurate.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R82-85]

+    /// Write buffer for the CSV a download exports, matching the 64 KB default the SD parsers read
+    /// with so neither side of the export is the narrow one.
+    /// </summary>
+    private const int CsvWriteBufferBytes = 64 * 1024;
Relevance

●●● Strong

Team often accepts small constant hygiene/DRY fixes; avoids drift and keeps buffer-size intent
centralized.

PR-#420
PR-#422

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a new 64 KB constant in DaqifiAgent and uses it as the CSV output FileStream buffer,
while Core already defines the SD parser default buffer size as 64 KB via
SdCardParseOptions.BufferSize. These are separate definitions that can diverge over time.

src/Daqifi.Mcp/DaqifiAgent.cs[81-86]
src/Daqifi.Mcp/DaqifiAgent.cs[871-876]
src/Daqifi.Core/Device/SdCard/SdCardParseOptions.cs[16-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DaqifiAgent` introduces `CsvWriteBufferBytes = 64 * 1024` with a comment stating it matches the SD parser read buffer, but the parser buffer is already defined by `SdCardParseOptions.BufferSize` (default 64 KB). Keeping two separate defaults can drift and makes future performance tuning error-prone.

## Issue Context
- `ExportCsvAsync` already has access to `parseOptions.BufferSize` (used for the input stream). If the intent is to keep read/write buffers aligned, the write-side should be derived from the same source rather than a second constant.

## Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[81-86]
- src/Daqifi.Mcp/DaqifiAgent.cs[871-876]

### Suggested change
- Remove `CsvWriteBufferBytes` and use `parseOptions.BufferSize` for the CSV output `FileStream` buffer size; OR
- If you want them independently tunable, rename/comment accordingly (remove the claim that it “matches” the parser), and consider centralizing defaults in a single shared constant.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Unsanitized filename in error ✓ Resolved 🐞 Bug ◔ Observability
Description
ResolveFileNameAsync interpolates the caller-provided fileName directly into an exception message,
but RequireFileName only trims/emptiness-checks. A filename containing control characters can
produce multiline/garbled MCP error output and confusing diagnostics.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R803-806]

+              + (files.Count > 20 ? $", and {files.Count - 20} more (call list_sd_files for all of them)." : ".");
+
+        throw new InvalidOperationException($"There is no file named '{fileName}' on the SD card. {available}");
+
Relevance

●●● Strong

Precedent: they accepted sanitizing user-supplied SD filenames to avoid unsafe output/injection;
same principle for exception text.

PR-#99

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The thrown message includes the raw fileName parameter, and the only local validation is
trim/empty. Core’s SD filename validation rejects control characters for safety, highlighting that
the MCP-layer message can still echo such characters before Core is reached.

src/Daqifi.Mcp/DaqifiAgent.cs[800-806]
src/Daqifi.Mcp/DaqifiAgent.cs[918-927]
src/Daqifi.Core/Device/SdCard/SdCardOperations.cs[1362-1374]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ResolveFileNameAsync` throws an `InvalidOperationException` that includes the raw `fileName` string. Since `RequireFileName` only trims and checks emptiness, a caller can supply control characters (e.g., `\n`, `\r`, tabs) that will be embedded into the exception message, potentially creating multiline/garbled output.

### Issue Context
Core already validates SD filenames for command-safety (`"`, `\n`, `\r`, `;`), but the "missing file" fast-fail occurs before Core validation and echoes the input into the error message.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[800-813]
- src/Daqifi.Mcp/DaqifiAgent.cs[918-927]

### Implementation notes
- Option A (preferred): strengthen `RequireFileName` to reject control chars and the same invalid characters Core rejects (at least `\r`, `\n`, `;`, `"`).
- Option B: escape/normalize the value used in the exception message (e.g., replace control chars with visible escape sequences) while still using the raw string for matching.
- Consider adding a small unit test that passes a filename with `\n` and asserts the thrown message does not contain a literal newline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous review results

Review updated until commit c4872f7

Results up to commit 2267912 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Orphaned CSV on export error ✓ Resolved 🐞 Bug ☼ Reliability
Description
ExportCsvAsync creates localPath + ".csv" before exporting; if CSV export/writes throw, the
partially-written CSV is left on disk. DownloadSdFileAsync then swallows the exception and returns
CsvPath=null, so callers can’t discover or clean up the orphaned temp file.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R857-860]

+            var csvPath = localPath + ".csv";
+            var fileStream = new FileStream(csvPath, FileMode.Create, FileAccess.Write, FileShare.Read);
+            await using (fileStream.ConfigureAwait(false))
+            {
Relevance

●●● Strong

Temp-file cleanup on export failure is a straightforward reliability fix; consistent with existing
CSV deletion on zero-samples.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CSV is created with FileMode.Create and exported into, but only the zero-sample case deletes
it; other exceptions propagate. The caller-level tool catches export exceptions and returns without
a CSV path, which makes any partially-created CSV file impossible for the caller to clean up.

src/Daqifi.Mcp/DaqifiAgent.cs[857-878]
src/Daqifi.Mcp/DaqifiAgent.cs[695-713]
src/Daqifi.Mcp/Dtos.cs[214-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ExportCsvAsync` can leave a partially written `*.csv` file behind when `CsvExporter.ExportAsync(...)` (or stream writing) throws. Since `DownloadSdFileAsync` catches and converts those failures into a successful tool response with `CsvPath=null`, the orphaned file becomes undiscoverable and accumulates in temp storage.

### Issue Context
- The CSV file is created eagerly (`FileMode.Create`) and only deleted in the special `SampleCount == 0` case.
- Any other exception during the export path leaves the file on disk.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[857-889]
- src/Daqifi.Mcp/DaqifiAgent.cs[695-713]

### Implementation notes
- Wrap the CSV creation/export block in a `try/catch` that calls `TryDelete(csvPath)` on *any* exception (including IO exceptions).
- Prefer writing to a temp name (e.g., `localPath + ".csv.tmp"`) and `File.Move(..., overwrite:true)` only after a successful export; on failure, delete the temp.
- Keep `OperationCanceledException` behavior consistent (either delete the temp file before rethrow or ensure the temp-name approach prevents litter).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
2. Unsanitized filename in error ✓ Resolved 🐞 Bug ◔ Observability
Description
ResolveFileNameAsync interpolates the caller-provided fileName directly into an exception message,
but RequireFileName only trims/emptiness-checks. A filename containing control characters can
produce multiline/garbled MCP error output and confusing diagnostics.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R803-806]

+              + (files.Count > 20 ? $", and {files.Count - 20} more (call list_sd_files for all of them)." : ".");
+
+        throw new InvalidOperationException($"There is no file named '{fileName}' on the SD card. {available}");
+
Relevance

●●● Strong

Precedent: they accepted sanitizing user-supplied SD filenames to avoid unsafe output/injection;
same principle for exception text.

PR-#99

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The thrown message includes the raw fileName parameter, and the only local validation is
trim/empty. Core’s SD filename validation rejects control characters for safety, highlighting that
the MCP-layer message can still echo such characters before Core is reached.

src/Daqifi.Mcp/DaqifiAgent.cs[800-806]
src/Daqifi.Mcp/DaqifiAgent.cs[918-927]
src/Daqifi.Core/Device/SdCard/SdCardOperations.cs[1362-1374]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ResolveFileNameAsync` throws an `InvalidOperationException` that includes the raw `fileName` string. Since `RequireFileName` only trims and checks emptiness, a caller can supply control characters (e.g., `\n`, `\r`, tabs) that will be embedded into the exception message, potentially creating multiline/garbled output.

### Issue Context
Core already validates SD filenames for command-safety (`"`, `\n`, `\r`, `;`), but the "missing file" fast-fail occurs before Core validation and echoes the input into the error message.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[800-813]
- src/Daqifi.Mcp/DaqifiAgent.cs[918-927]

### Implementation notes
- Option A (preferred): strengthen `RequireFileName` to reject control chars and the same invalid characters Core rejects (at least `\r`, `\n`, `;`, `"`).
- Option B: escape/normalize the value used in the exception message (e.g., replace control chars with visible escape sequences) while still using the raw string for matching.
- Consider adding a small unit test that passes a filename with `\n` and asserts the thrown message does not contain a literal newline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 39ebfca ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Sync CSV output stream ✓ Resolved 🐞 Bug ➹ Performance
Description
ExportCsvAsync opens the CSV output FileStream with default (non-async) options while
CsvExporter.ExportAsync performs many WriteAsync calls; this can fall back to synchronous file
I/O / thread-pool-assisted writes and reduce scalability and cancellation responsiveness during
large exports.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R865-868]

+                var fileStream = new FileStream(csvPath, FileMode.Create, FileAccess.Write, FileShare.Read);
+                await using (fileStream.ConfigureAwait(false))
+                {
+                    var writer = new StreamWriter(fileStream);
Relevance

●●● Strong

They favor true-async patterns; output stream should match async exporter writes for
scalability/cancellation.

PR-#94
PR-#400

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new code creates the CSV output stream using the non-async FileStream constructor, but the
exporter uses async write APIs throughout, so the export path is not truly async on the output side.

src/Daqifi.Mcp/DaqifiAgent.cs[865-873]
src/Daqifi.Core/Logging/Export/CsvExporter.cs[28-33]
src/Daqifi.Core/Logging/Export/CsvExporter.cs[90-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ExportCsvAsync` writes CSV using `CsvExporter.ExportAsync(...)` which uses async `TextWriter.WriteAsync` heavily, but the CSV output `FileStream` is created without `useAsync: true` (and without an explicit buffer size). For large SD logs, this can degrade throughput and responsiveness because the async export can end up performing synchronous file writes (often via thread-pool assistance), and cancellation may be less responsive.

### Issue Context
Input reading already uses `useAsync: true`; output should be consistent.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[865-874]

### Proposed fix
Create the CSV `FileStream` with `useAsync: true` and an explicit buffer size, e.g.:
```csharp
var fileStream = new FileStream(
   csvPath,
   FileMode.Create,
   FileAccess.Write,
   FileShare.Read,
   bufferSize: 64 * 1024, // or a shared constant
   useAsync: true);
```
Optionally also construct `StreamWriter` with an explicit encoding/buffer if desired.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread src/Daqifi.Mcp/DaqifiAgent.cs
Comment thread src/Daqifi.Mcp/DaqifiAgent.cs
…SD file names

Qodo round 1. A CSV export that throws after the file was created left it
on disk with CsvPath=null, so nobody could name it to clean it up; it is
now deleted on every failure path including cancellation. And the listing
pre-flight ran before Core's SCPI-safety check, so a name containing
newlines produced a multi-line 'no such file' message instead of a plain
rejection — the same character rule now applies at the tool boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread src/Daqifi.Mcp/DaqifiAgent.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 39ebfca

Qodo round 2. CsvExporter writes every row through WriteAsync, but the
output FileStream was opened without useAsync, so those went through the
thread pool rather than the OS async path — while the input stream it
reads from already used it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread src/Daqifi.Mcp/DaqifiAgent.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ceb98eb

Qodo round 3. The comment asserted a relationship nothing enforces, so
tuning one side would have made it a lie. The two buffers stay separate
— read-a-log-at-a-time is not write-a-CSV-at-a-time — and the doc now
says that instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c4872f7

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review.

Round 4 on head c4872f7: Bugs (0) / Rule violations (0) / Skill insights (0), all four earlier findings struck through as resolved, 0 unresolved threads (4 total, all resolved). Freshness checked rather than assumed — the review body carries 26 references to c4872f7 against 1 each to the three earlier commits. Settle re-check at +5.5 min: summary byte-identical, comment count unchanged, threads still 0. CI build SUCCESS; MERGEABLE / CLEAN.

Rounds 1-3 took: a half-written CSV left on disk when an export threw (now deleted on every failure path, cancellation included); a file name with control characters reaching my listing pre-flight ahead of Core’\s SCPI-safety check and producing a multi-line "no such file" message (the same character rule now applies at the tool boundary, with tests on both sides of it); and the exported CSV being opened for synchronous writes while the log it reads was already async. Round 3 was a comment of mine claiming the CSV write buffer tracked the parse buffer — it does not, and the two are deliberately independent, so the doc changed rather than the code.

Full suite re-run green after every push: net9.0 (2964 Core + 95 Mcp) and net10.0 (2964), 0 failures, 0 warnings.

Not merging — this is for your review.

@tylerkron
tylerkron added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 23920e0 Aug 13, 2026
1 check passed
@tylerkron
tylerkron deleted the feat/mcp-sd-data-tools-500 branch August 13, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(mcp): SD-card data tools — an agent can start a log it can never list, download, or read

1 participant